MVVM in ZK 6 - Design CRUD page by MVVM pattern
Dennis Chen, Senior Engineer, Potix Corporation
November 14, 2011
ZK 6 FL-2012-01-11 and after
In the previous article Design your first MVVM page I have demonstrated how ZK Bind can be used to accomplish a search page by the MVVM pattern. In this article, I will demonstrate the whole process of how a common CRUD page can be designed using MVVM pattern, including the creation, validation when editing and confirmation when deleting.
Case scenario
In this article, I will use an Order Management scenario to show you the concepts. In an Order Management page, you can list the Orders, select one to see the details and modify it and you can also create a new Order or delete an Order. I will also use 3 cases of ViewModel and View to demonstrate some issues that may arise when designing a MVVM page and how you can come about solving it.
Design the ViewModel
Before designing the ViewModel, we have to firstly design a domain object. In this example, the domain object is an "Order" class which contains the following fields: "id", "description", "price", "quantity", "creationDate" and "shippingDate". Below is the partial code of the "Order"
Domain Object : Order.java
public class Order {
String id;
String description;
double price;
int quantity;
Date creationDate;
Date shippingDate;
//getter
@DependsOn( { "price", "quantity" })
public double getTotalPrice() {
return price * quantity;
}
...
//setter
...
}
In this domain object, I added a ZK Bind annotation @DependsOn
, It is a special annotation to establish dependency for a dynamic value. For example, "totalPrice" is calculated by multiplying "price" and "quantity". It does not have a field to store its value, hence "totalPrice" depends on "price" and "qantity". When binder gets any change notifications from property "price" or "quantity", it will also mark property "totalPrice" as changed.
ViewModel : OrderVM.java
public class OrderVM {
ListModelList<Order> orders;//the order list
Order selected;//the selected order
public ListModelList<Order> getOrders() {
if (orders == null) {
orders = new ListModelList<Order>(getService().list());//init the list
}
return orders;
}
@NotifyChange("selected")
public void setSelected(Order selected) {
this.selected = selected;
}
//action commands
@Command @NotifyChange({"selected","orders"})
public void newOrder(){
Order order = new Order();
getOrders().add(order);
selected = order;//select the new one
}
@Command @NotifyChange("selected")
public void saveOrder(){
getService().save(selected);
}
@Command @NotifyChange({"selected","orders"})
public void deleteOrder(){
getService().delete(selected);//delete selected
getOrders().remove(selected);
selected = null; //clean the selected
}
public OrderService getService() {...}
//other getter …
//validators
...
}
In the example above, I used a "OrderService" to "list()", "save()" and "delete()" orders to establish an isolation between ViewModel and business logic. In command method "newOrder()", I created a new "order" object. In command method "saveOrder()", I saved the selected "order". In command method "deleteOrder()", I deleted the selected "order" and removed it from the order list. In all command methods, I added @Command
annotations to to them and I also added corresponding @NotifyChange
annotations to hint changed properties caused by their corresponding command methods.
Design the View & Run
Following is the preview of a View binding with a ViewModel. I used a listbox to display the orders, 3 buttons to perform the New, Save and Delete actions, and a grid with textbox, datebox to edit the details.
The View
A few basic binding concepts were mentioned in the previous article I will explain it again briefly here. For detailed information, please visit the referred article. Basically, I have set the apply
attribute to org.zkoss.bind.BindComposer
as the composer and bind the viewModel
to "OrderVM' that I have just created (with the variable name vm). I also binded the "model" of listbox to "vm.orders" and "selectedItem" to "vm.selected". In order to show each order of the model, I binded specified properties in the template labelled with predefined converter. I also binded 3 buttons to 3 commands to perform actions on the buttons. By binding "selectedItem" to "vm.seleted", when an order is selected, the Save and Delete buttons are only clickable while the groupbox of the editor part is visible.
View : order.zul
<window title="Order Management" border="normal" width="600px"
apply="org.zkoss.bind.BindComposer" viewModel="@id('vm') @init('org.zkoss.bind.examples.order.OrderVM')"
validationMessages="@id('vmsgs')">
<listbox model="@load(vm.orders)" selectedItem="@bind(vm.selected)" hflex="true" height="200px">
… <template name="model" var="item">
<listitem >
<listcell label="@load(item.id)"/>
<listcell label="@load(item.quantity)"/>
<listcell label="@load(item.price) @converter('formatedNumber', format='###,##0.00')"/>
<listcell label="@load(item.creationDate) @converter('formatedDate', format='yyyy/MM/dd')"/>
<listcell label="@load(item.shippingDate) @converter('formatedDate', format='yyyy/MM/dd')"/>
</listitem>
</template>
</listbox>
<toolbar>
<button label="New" onClick="@command('newOrder')" />
<button label="Save" onClick="@command('saveOrder')" disabled="@load(empty vm.selected)" />
<button label="Delete" onClick="@command('deleteOrder')" disabled="@load(empty vm.selected)" />
</toolbar>
<groupbox visible="@load(not empty vm.selected)" hflex="true" mold="3d">
<grid hflex="true" >
… <rows>
<row>Id <label value="@load(vm.selected.id)"/></row>
<row>Description <textbox value="@load(vm.selected.description)"/></row>
<row>Quantity
<hlayout>
<intbox id="qbox" value="@bind(vm.selected.quantity) @validator(vm.quantityValidator)"/>
<label value="@load(vmsgs[qbox])" sclass="red" />
</hlayout>
</row>
<row>Price
<hlayout>
<doublebox id="pbox" value="@bind(vm.selected.price) @validator(vm.priceValidator)" format="###,##0.00" />
<label value="@load(vmsgs[pbox])" sclass="red" />
</hlayout>
</row>
<row>Total Price <label value="@load(vm.selected.totalPrice) @converter('formatedNumber', format='###,##0.00')" /></row>
<row>Creation Date
<hlayout>
<datebox value="@bind(vm.selected.creationDate)"/>
</hlayout>
</row>
<row>Shipping Date
<hlayout>
<datebox value="@bind(vm.selected.shippingDate)" />
</hlayout>
</row>
</rows>
</grid>
</groupbox>
</window>
To display validation messages, I binded validationMessages
, which provides a built-in validation messages display mechanism, to 'vmsgs'.
Now, I am going to introduce the editor part. By binding a bean property to a user editable attribute of a component (ex, the "value" of intbox), it creates two way binding (load-binding and save-binding), which means, not only the property can be loaded to the attribute of the component but the attribute can also be saved to the property if user has edited it. We also introduced the @validator(expresson)
syntax which enables the validation before saving data to the bean property.
In the example above, I have binded the value of a doublebox to "vm.selected.price" and also used the @validator(vm.priceValidator)
annotation to provide a Validator to execute validation when saving data to "vm.selected.price". The value of "label" is also binded to "vmsgs[pbox]" in order to show the message in cases where validation fails in component 'pbox'.
Validator in ViewModel
Since I bound View with ViewModel with some validators, I have to provide the validator in the ViewModel. Note that validators can also come from other beans. It all depends on the expression in @validator(expression)
. However, providing it in the ViewModel is the easiest way to demonstrate its implementation.
ViewModel : OrderVM.java
public Validator getPriceValidator(){
return new AbstractValidator(){
public void validate(ValidationContext ctx) {
Double price = (Double)ctx.getProperty().getValue();
if(price==null || price<=0){
addInvalidMessage(ctx, "must be larger than 0");
}
}
};
}
public Validator getQuantityValidator(){
return new AbstractValidator(){
public void validate(ValidationContext ctx) {
Integer quantity = (Integer)ctx.getProperty().getValue();
if(quantity==null || quantity<=0){
addInvalidMessage(ctx, "must be larger than 0");
}
}
};
}
Two validators are provided: "priceValidator" and 'quantityValidator', they are used to validate the values of price and quantity respectively in which the values has to be greater than 0. When implementing the validator, you can get the main property that needs to be validated by ValidationContext.getProperty().getValue()
, if the value is not valid, set invalid by calling AbstractValidator.addInvalidMessage(ctx,message)
. In most cases, some kind of message needs to be shown if the value is not valid.
Showcase 1
Issues
The above implementation looks straight forward but it has some issues when editing an order.
- When editing a field, such as the price, the value is directly updated to the bean if it is a valid value i.e. larger than 0, however it is not automatically saved before you click the save button. So if you edit a price and move to another order, you will see that the value was changed in order list, but it is not saved by service.
- When editing a field, such as the price, after editing the field, if the value is not valid, you will see the validation message while the bean still contains the old value. And you can still click the save button to save it by service with the old value.
- When creating a new order, by clicking save directly, the values are all not been verified and saved.
In the next section, I will introduce the batch saving and validation concept of a command to solve these issues.
Batch saving of a command
What is a Command Execution
A command execution is a mechanism of ZK Bind where it performs a method call on the ViewModel. It binds to a component's event and when a binding event comes, binder will follow the lifecycle to complete the execution. There are 6 phases in the COMMAND
execution: VALIDATION
, SAVE-BEFORE
, LOAD-BEFORE
, EXECUTE
, SAVE-AFTER
, LOAD-AFTER
.
Saving and Loading in Command Execution
You could save multiple values to ViewModel at the same time before or after the EXECUTE
phase (i.e., call the ViewModel's command method) by using the @save(expression, before|after='a-command')
syntax (use this syntax, the value will not be saved immediately after being edited by user). You could also load values to a component before or after the EXECUTE
phase by using the @load(expression, before|after='a-command')
syntax.
Validation in Command Execution
Validation is also included in the command execution. It is performed in the VALIDATION
phase before any other phases. If there are multiple save binding that depends on the same command, all validators of binding will be called in the VALIDATION
phase. If a validator said invalid; the execution will be broken, and ignored in the remaining phases.
Phases of Command Execution
Following is the phases of a Command Execution:
- When a bound ZK event enters the binder, the
COMMAND
phase will be invoked and all phases within theCOMMAND
phase will start to execute one by one - In the
VALIDATION
phase, binder first collects all the properties that needs to be verified. Then, it calls each validator of save-binding that is related to this command. In each call to a validator, binder provides a newValidationContext
which contains the main property and other collected properties. This means, you can do dependent validation with collected properties, for example, checking whether the shipping date is larger than the creation date. If any validator reports invalid by callingValidationContext.setInvalid ()
orAbstractValidator.addInvalidMessage()
, binder ignores all other phases and loads any other properties that has been notified for a change, for example by callingBinder.notifyChange()
. - In the
SAVE-BEFORE
phase, binder calls all the save-binding that is relative to the command and mark "before" to save the value to the expression - In the
LOAD-BEFORE
phase, binder calls all the load-binding that is relative to the command and mark "before" to load the value from expression to the component - In the
EXECUTE
phase, binder calls the command method of the ViewModel. For example, if the command is "saveOrder", it will try to find a method that has annotation@Command('saveOrder')
or a method which is namedsaveOrder()
with@Command()
of the viewModel, and then call it. If there is no method to execute, it complains with an exception. - In the
SAVE-AFTER
phase, binder calls all the save-binding that is relative to the command and mark "after" to save the value to the expression - In the
LOAD-AFTER
phase, binder calls all the load-binding that is relative to the command and mark "after" to load the value from expression to component
Redesign for Batch Saving & Validation
I will use the command execution concept to solve issues that I mentioned in the first example. To resolve the issues, I changed the binding of View to the following.
View : order2.zul
<window title="Order Management" border="normal" width="600px"
apply="org.zkoss.bind.BindComposer" viewModel="@id('vm') @init('org.zkoss.bind.examples.order.OrderVM2')"
validationMessages="@id('vmsgs')">
...
<rows>
<row>Id <label value="@load(vm.selected.id)"/></row>
<row>Description <textbox value="@load(vm.selected.description) @save(vm.selected.description, before='saveOrder')"/></row>
<row>Quantity
<hlayout>
<intbox id="qbox" value="@load(vm.selected.quantity) @save(vm.selected.quantity, before='saveOrder')
@validator(vm.quantityValidator)"/>
<label value="@load(vmsgs[qbox])" sclass="red" />
</hlayout>
</row>
<row>Price
<hlayout>
<doublebox id="pbox" value="@load(vm.selected.quantity) @save(vm.selected.quantity, before='saveOrder')
@validator(vm.priceValidator)" format="###,##0.00" />
<label value="@load(vmsgs[pbox])" sclass="red" />
</hlayout>
</row>
<row>Total Price <label value="@load(vm.selected.totalPrice) @converter('formatedNumber', format='###,##0.00')" /></row>
<row>Creation Date
<hlayout>
<datebox id="cdbox" value="@load(vm.selected.creationDate) @save(vm.selected.creationDate,before='saveOrder')
@validator(vm.creationDateValidator)"/>
<label value="@load(vmsgs[cdbox])" sclass="red" />
</hlayout>
</row>
<row>Shipping Date
<hlayout>
<datebox id="sdbox" value="@load(vm.selected.shippingDate) @save(vm.selected.shippingDate,before='saveOrder')
@validator(vm.shippingDateValidator)"/>
<label value="@load(vmsgs[sdbox])" sclass="red" />
</hlayout>
</row>
</rows>
...
</window>
</zk>
All @bind(expression)
syntax in the grid has been changed to the @load(expression) @save(expression, before='a-command')
syntax. For example, @load(vm.selected.price) @save(vm.selected.price,before='saveOrder')
, so the intbox loads when "vm.selected.price" is changed, and only save before executing the "saveOrder" command (before calling saveOrder() of ViewModel)
I also added @validator(expression)
to datebox to do dependent validation( shippingDate has to be larger than creationDate). The new validator is provided by the ViewModel, so I created a new OrderVM2 extended from OrderVM to provide the new validator.
ViewModel : OrderVM2.java
public class OrderVM2 extends OrderVM{
//validators for command
public Validator getCreationDateValidator(){
return new AbstractValidator(){
public void validate(ValidationContext ctx) {
Date creation = (Date)ctx.getProperty().getValue();
if(creation==null){
addInvalidMessage(ctx,"must be not null");
}
}
};
}
public Validator getShippingDateValidator(){
return new AbstractValidator(){
public void validate(ValidationContext ctx) {
Date shipping = (Date)ctx.getProperty().getValue();//the main property
Date creation = (Date)ctx.getProperties("creationDate")[0].getValue();//the dependent
//do mixed validation, shipping date have to large than creation more than 3 days.
if(!CaldnearUtil.isDayAfter(creation,shipping,3)){
addInvalidMessage(ctx,"must be larger than creation date at least 3 days");
}
}
};
}
}
In the implementation of shippingDateValidator
, I get the shipping date directly by ValidationContext.getProperty().getValue()
since it is the main property of this binding. To get any other dependent properties, I used ValidationContext.getProperties(property)
to get the Property
array (because you might bind to different beans with the same property name in the same command, such as "vm.i1.price" and "vm.i2.price", in this case you will get 2 properties with the name "price" but with base object being "i1" and "i2" respectively), and since there is only one "creationDate" property in this case, I can get the value from the first Property directly. Then, I simply compare the dates to execute the validation.
Showcase 2
Show Dialog
It is very important to ask users when he/she tries to do something critical, such as deleting an order. In this section, I am going to talk about how you can show a question dialog
Redesign for Showing a Question Dialog
Back to thinking about the ViewModel, there will be a 2 new methods - to show and hide the dialog, and we will also need a message to display to users. I have created an OrderVM3 which is extended from OrderVM2 to provide the method and message.
ViewModel : OrderVM3.java
public class OrderVM3 extends OrderVM2{
//message for confirming the deletion.
String deleteMessage;
@Override
@Command @NotifyChange({"selected","orders","deleteMessage"})
public void deleteOrder(){
super.deleteOrder();
deleteMessage = null;
}
@Command @NotifyChange("deleteMessage")
public void confirmDelete(){
//set the message to show to user
deleteMessage = "Do you want to delete "+selected.getId()+" ?";
}
@Command @NotifyChange("deleteMessage")
public void cancelDelete(){
//clear the message
deleteMessage = null;
}
//getter
}
In OrderVM3, I provided a deleteMessage
file and added 2 new methods either to set or to clean it. I also overwrote deleteOrder
as I need to clear the message after the order is deleted.
From the View concept, the dialog shows when the message has value and hides when the message is empty. Moreover, I do not want to present the dialog to ask users to delete when the selected order is not saved yet.
View : order3.zul
<window title="Order Management" border="normal" width="600px"
apply="org.zkoss.bind.BindComposer" viewModel="@id('vm') @init('org.zkoss.bind.examples.order.OrderVM3')" >
...
<toolbar>
...
<!-- show confirm dialog when selected is persisted -->
<button label="Delete" onClick="@command(empty vm.selected.id?'deleteOrder':'confirmDelete')" disabled="@load(empty vm.selected)" />
</toolbar>
...
<window title="Confirm" mode="modal" border="normal" width="300px" visible="@load(not empty vm.deleteMessage)">
<vbox hflex="true">
<hlayout height="50px">
<image src="~./zul/img/msgbox/question-btn.png"/>
<label value="@load(vm.deleteMessage)"/>
</hlayout>
<hbox pack="center" hflex="true">
<button label="Delete" onClick="@command('deleteOrder')"/>
<button label="Cancel" onClick="@command('cancelDelete')"/>
</hbox>
</vbox>
</window>
</window>
To show a dialog, I used a modal window and bind its visible
to not empty vm.deleteMessage
. It is really easy to show or hide a dialog this way by using ZK Bind. To perform a command, in the dialog, it has two buttons bound to commands deleteOrder
and cancelDelete
respectively. deleteOrder
is the old command to delete the order and cancelDelete
is the new command to clean the message.
As a second requirement, I only want to show the dialog when the order is persisted. Depending on the implementation of this example, an order is persisted when it has an id. I bound the button with @command(empty vm.selected.id?'deleteOrder':'confirmDelete')
, and upon clicking on the button, depending on whether the selected order has an id or not, it will execute either the deleteOrder
or the confirmDelete
command.
Showcase 3
Syntax review
ZUL annotation syntax
Syntax | Explanation |
---|---|
validationMessages="@id(name)" | Sets the ValidationMessages
|
@validator(expression, arg = arg-expression) | Provides a validator for a binding
|
comp-attribute="@save(expression, before|after='a-command')" | Provide a binding to save component's attribute to a property of the expression
|
Java syntax
Syntax | Explanation |
---|---|
@DependsOn on getter | Mark the properties that depends on other properties, the return value of the getter is usually a dynamic calculated value.
|
Summary
In this article, I have demonstrated how you can create a real CRUD page by the MVVM pattern with 3 cases. From 'The save-binding with a validator' to 'Batch saving and validation by a command' and finally to 'Showing a dialog for confirming a deletion'. There will be more upcoming articles for this series of articles on MVVM and ZK 6, before then, any feedback is welcomed.
Downloads
[zbindexamples ] : You can download deployable war files here, it also contains source code of the examples in this article
Comments
Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License. |